require_relative "./Exprs/IdentExpr"
require_relative "./Exprs/NumExpr"
require_relative "./Exprs/ParamsExpr"
require_relative "./Exprs/FuncExpr"
require_relative "./Exprs/RatExpr"
require_relative "./Exprs/BinopExpr"
require_relative "./Exprs/BracedExpr"
require_relative "./Exprs/UnaryExpr"
require_relative "./Exprs/AssignExpr"
require_relative "./Exprs/ApplyExpr"

# The parser for the Calcula language written in Ruby. Please, if adding a
# grammar rule, document it.
#
# @author Paul T.
class Calcula::Parser

  include Calcula::Exprs

  # An error that is raised because of parsing failures.
  #
  # @author Paul T.
  class ParserError < StandardError
  end

  EQL_LIKE_OPS = [:OP_EQ, :OP_NE, :OP_LT, :OP_GT, :OP_LE, :OP_GE]
  MUL_LIKE_OPS = [:OP_REM, :OP_MUL, :OP_DIV, :PAREN_O]
  ADD_OR_SUB = [:OP_ADD, :OP_SUB]
  STMT_TERMINATOR = [:DISP, :ASSERT]

  # Constructs a new parser with the specified Tokens
  #
  # @param toks [Array<Calcula::Token>] The tokens most likely generated from the lexer
  def initialize(toks)
    @toks = toks
    @i = 0
  end

  # Raises an error with seemingly useful message
  #
  # @param unexpectedTok [Calcula::Token, nil] The token that was found instead, nil if End of file was found.
  # @param msg [String] The additional message or the reason of the error
  # @raise [RuntimeError] Calling this method raises this error
  def parseError(unexpectedTok, msg)
    if unexpectedTok == nil then
      raise ParserError, "Unexpected EOF: #{msg != '' ? msg : '<no message>'}"
    end
    raise ParserError, "Unexpected token #{unexpectedTok}: #{msg != '' ? msg : '<no message>'}"
  end

  # Performs a lookahead and checks to see if the next token is the one specified.
  # If the specified token is not found, then the method `parseError` is called.
  #
  # (see #lookahead)
  # (see #parseError)
  # @param tokId [Symbol] The type of token being expected
  # @return [Calcula::Token] The actual token found
  def consume(tokId)
    cons = lookahead()
    if cons == nil then
      parseError(nil, "Expected token #{tokId}")
    elsif cons.id == tokId then
      @i += 1
      return cons
    else
      parseError(@toks[@i], "Expected token #{tokId}")
    end
  end

  # Finds the next non-ignored token and returns it
  #
  # @return [Calcula::Token, nil] nil if the next non-ignored token nil. This could mean end of script.
  def lookahead
    return nil if @toks[@i] == nil
    while @toks[@i].id == :WS || @toks[@i].id == :COMMENT do
      @i += 1
      return nil if @toks[@i] == nil
    end
    if @i < @toks.length then
      return @toks[@i]
    else
      return nil
    end
  end

  # Begins to parse the list of tokens and tries to convert it into an expression tree
  #
  # @return [Calcula::Expr, nil] If an expression tree can be constructed, nil otherwise
  def parse
    tmp = lookahead
    if tmp == nil then
      nil
    else
      case tmp.id
      when :LET then
        defExpr
      when :COMMA then
        consume(:COMMA) # Comma is like semicolons in ruby. Optional!
        parse
      else
        printExpr
      end
    end
  end

  # Parses the syntax rule of expr
  #   equation = logicNotExpr
  #
  # @return [Calcula::Expr]
  def equation
    logicNotExpr()
  end

  # Parses the syntax rule of defExpr
  #   defExpr = LET identExpr PAREN_O paramsExpr PAREN_C OP_EQ equation
  #           | LET identExpr OP_EQ equation
  # @return [Calcula::Expr]
  def defExpr
    consume(:LET)
    name = identExpr()
    val = nil

    maybeParenO = lookahead
    parseError(nil, "Expected a complete function declaration or '='") if maybeParenO == nil

    case maybeParenO.id
    when :PAREN_O then
      consume(:PAREN_O)
      params = paramsExpr()
      consume(:PAREN_C)
      consume(:OP_EQ)
      act = equation()
      val = FuncExpr.new(params, act)
    when :OP_EQ then
      consume(:OP_EQ)
      val = equation()
    else
      parseError(maybeParenO, "Expected complete function declaration or '='")
    end
    AssignExpr.new(name, val)
  end

  # Parses the syntax ruke for printExpr
  #   printExpr = equation (DISP | ASSERT)?
  #
  def printExpr
    base = equation()
    maybeDisp = lookahead()
    if maybeDisp != nil && STMT_TERMINATOR.index(maybeDisp.id) != nil then
      UnaryExpr.mkPostfix(consume(maybeDisp.id), base)
    else
      base
    end
  end

  # Parses the syntax rule of logicNotExpr
  #   logicNotExpr = NOT logicOrExpr
  #
  # @return [Calcula::Expr]
  def logicNotExpr
    tmp = lookahead
    parseError(nil, "Expected an equation") if tmp == nil
    if tmp.id == :NOT then
      tmp = consume(tmp.id)
    end
    rst = logicOrExpr
    if tmp.id == :NOT then
      rst = UnaryExpr.mkPrefix(tmp, rst)
    end
    return rst
  end

  # Performs lookahead and constructs a parse tree. This could only be used on
  # parse trees with the grammar of
  #   self = $1:child ($2:SOMETHING $3:child)*
  #
  # The tree being constructed must have the initialization sequence of
  #   SubclassOfExpr.new($2, $1, $3)
  #
  # @param childrenMethod [Symbol] The name of `child` in the example grammar
  # @param exprClass [Constant (class name)] The `SubclassOfExpr` in the example
  # @yield [id, text] Predicate for whether or not lookahead should continue
  # @yieldparam id [Symbol] The current lookahead token's id
  # @yieldparam text [String] The current lookahead token's text
  # @yieldreturn [true, false] true if consumption should continue, false otherwise
  # @return [Calcula::Expr] The tree being constructed
  def consumeWhen(childrenMethod, exprClass) # => Take a block (predicate)
    rst = self.send(childrenMethod)
    tmp = lookahead
    while tmp != nil && yield(tmp.id, tmp.text) do
      rst = exprClass.new(consume(tmp.id), rst, self.send(childrenMethod))
      tmp = lookahead
    end
    return rst
  end

  # Parses the syntax rule of logicOrExpr
  #   logicOrExpr = logicAndExpr (OR logicAndExpr)*
  #
  # @return [Calcula::Expr]
  def logicOrExpr
    return consumeWhen(:logicAndExpr, BinopExpr) do |id, _txt|
      id == :OR
    end
  end

  # Parses the syntax rule of logicAndExpr
  #   logicAndExpr = equalLikeExpr (AND equalLikeExpr)*
  #
  # @return [Calcula::Expr]
  def logicAndExpr
    return consumeWhen(:equalLikeExpr, BinopExpr) do |id, _txt|
      id == :AND
    end
  end

  # Parses the syntax rule of equalLikeExpr
  #   equalLikeExpr = addLikeExpr ((OP_EQ | OP_NE | OP_LT | OP_GT | OP_LE | OP_GE) addLikeExpr)?
  #
  # @return [Calcula::Expr]
  def equalLikeExpr
    return consumeWhen(:addLikeExpr, BinopExpr) do |id, _txt|
      EQL_LIKE_OPS.index(id) != nil
    end
  end

  # Parses the syntax rule of addLikeExpr
  #   addLikeExpr = mulLikeExpr ((OP_ADD | OP_SUB) mulLikeExpr)*
  #
  # @return [Calcula::Expr]
  def addLikeExpr
    return consumeWhen(:mulLikeExpr, BinopExpr) do |id, _txt|
      ADD_OR_SUB.index(id) != nil
    end
  end

  # Parses the syntax rule of mulLikeExpr
  #   mulLikeExpr = prefixExpr ((OP_REM | OP_MUL | OP_DIV) mulLikeExpr)?
  #               | prefixExpr (PAREN_O (equation COMMA)+ PAREN_C mulLikeExpr)?
  #
  # @return [Calcula::Expr]
  def mulLikeExpr
    rst = prefixExpr
    tmp = lookahead
    while tmp != nil && MUL_LIKE_OPS.index(tmp.id) != nil do
      if tmp.id == :PAREN_O then
        consume(:PAREN_O)
        appliedOn = [equation]
        loop do
          begin
            consume(:COMMA)
          rescue ParserError
            break
          end
          appliedOn << equation
        end
        consume(:PAREN_C)
        rst = ApplyExpr.new(rst, appliedOn)
      else
        rst = BinopExpr.new(consume(tmp.id), rst, prefixExpr)
      end
      tmp = lookahead
    end
    return rst
  end

  # Parses the syntax rule for prefixExpr
  #   prefixExpr = (OP_SUB | OP_ADD)? postfixExpr
  #
  # @return [Calcula::Expr]
  def prefixExpr
    tmp = lookahead
    parseError(nil, "Expected a prefix operator or equation") if tmp == nil

    case tmp.id
    when :OP_SUB, :OP_ADD then
      op = consume(tmp.id)
      return UnaryExpr.mkPrefix(op, postfixExpr)
    else
      return postfixExpr
    end
  end

  # Parses the syntax rule for postfixExpr
  #   postfixExpr = exponentExpr ROUND_DOLLAR?
  #
  # @return [Calcula::Expr]
  def postfixExpr
    base = exponentExpr
    maybePostfix = lookahead
    if maybePostfix != nil then
      case maybePostfix.id
      when :ROUND_DOLLAR then
        return UnaryExpr.mkPostfix(consume(:ROUND_DOLLAR), base)
      end
      # Default case fallsthough and base itself gets returned
    end
    return base
  end

  # Parses the syntax rule for exponentExpr
  #   exponentExpr = parenExpr (OP_POW parenExpr)*
  #
  # @return [Calcula::Expr]
  def exponentExpr
    return consumeWhen(:parenExpr, BinopExpr) do |id, _txt|
      id == :OP_POW
    end
  end

  # Parses the syntax rule of parenExpr
  #   parenExpr = PAREN_O equation PAREN_C
  #             | SQUARE_O equation SQUARE_C
  #             | basicValExpr
  #
  # @return [Calcula::Expr]
  def parenExpr
    tmp = lookahead()
    parseError(nil, "Expected PAREN_O or an equation") if tmp == nil

    case tmp.id
    when :PAREN_O then
      head = consume(:PAREN_O)
      inner = equation()
      tail = consume(:PAREN_C)
      return BracedExpr.new(head, tail, inner)
    when :SQUARE_O then
      head = consume(:SQUARE_O)
      inner = equation()
      tail = consume(:SQUARE_C)
      return BracedExpr.new(head, tail, inner)
    else return basicValExpr()
    end
  end

  # Parses the syntax rule for basicValExpr
  #   basicValExpr = numExpr | composeExpr | lambdaExpr
  #
  # @return [Calcula::Expr]
  def basicValExpr
    tmp = lookahead()
    parseError(nil, "Expected numExpr or identExpr") if tmp == nil

    case tmp.id
    when :NUM then
      return numExpr()
    when :ID then
      return composeExpr()
    when :LAMBDA then
      return lambdaExpr()
    else parseError(tmp, "Expected numExpr or identExpr")
    end
  end

  # Parses the syntax rule for composeExpr
  #   composeExpr = identExpr (COMPOSE identExpr)*
  #
  # @return [Calcula::Expr]
  def composeExpr
    return consumeWhen(:identExpr, BinopExpr) do |id, _txt|
      id == :COMPOSE
    end
  end

  # Parses the syntax rule for paramsExpr
  #   paramsExpr = ID (COMMA ID)+
  #              | ID
  #
  # @return [Calcula::Expr]
  def paramsExpr
    # NOTE: Do not change this part to using the `consumeWhen` method.
    #       it does not work like that.
    params = [consume(:ID)]
    loop do
      begin
        consume(:COMMA)
      rescue ParserError
        break
      end
      params << consume(:ID)
    end
    return ParamsExpr.new(params)
  end

  # Parses the syntax rule for lambdaExpr
  #   lambdaExpr = LAMBDA paramsExpr equation
  #
  # @return [Calcula::Expr]
  def lambdaExpr
    consume(:LAMBDA)
    params = paramsExpr()
    return FuncExpr.new(params, equation())
  end

  # Parses the syntax rule for numExpr
  #   numExpr = NUM (RAT NUM)?
  #
  # @return [Calcula::Expr]
  def numExpr
    tok = consume(:NUM)
    maybeRat = lookahead()
    if maybeRat != nil and maybeRat.id == :RAT then
      consume(:RAT)
      bottom = consume(:NUM)
      return RatExpr.new(tok, bottom)
    else
      return NumExpr.new(tok)
    end
  end

  # Parses the syntax rule for identExpr
  #   identExpr = ID
  #
  # @return [Calcula::Expr]
  def identExpr
    tok = consume(:ID)
    return IdentExpr.new(tok)
  end

  # Resets the state of the parser
  def reset
    @i = 0
  end
end
